package parasoft.centrap.test;

import java.io.*;

/**
 * Convenience methods for executing non-Java processes.
 * More information about this class is available from <a target="_top" href=
 * "http://ostermiller.org/utils/ExecHelper.html">ostermiller.org</a>.
 *
 * @author Stephen Ostermiller http://ostermiller.org/contact.pl?regarding=Java+Utilities
 * @since ostermillerutils 1.06.00
 */
public final class ExecHelper {

  /**
   * Executes the specified command and arguments in a separate process, and waits for the
   * process to finish.
   * <p>
   * Output from the process is expected to be text in the system's default character set.
   * <p>
   * No input is passed to the process on STDIN.
   *
   * @param cmdarray array containing the command to call and its arguments.
   * @return The results of the execution in an ExecHelper object.
   * @throws SecurityException if a security manager exists and its checkExec method doesn't allow creation of a subprocess.
   * @throws IOException - if an I/O error occurs
   * @throws NullPointerException - if cmdarray is null
   * @throws IndexOutOfBoundsException - if cmdarray is an empty array (has length 0).
   *
   * @since ostermillerutils 1.06.00
   */
  public static ExecHelper exec(String[] cmdarray) throws IOException {
    return new ExecHelper(Runtime.getRuntime().exec(cmdarray), null);
  }

  /**
   * Executes the specified command and arguments in a separate process, and waits for the
   * process to finish.
   * <p>
   * Output from the process is expected to be text in the system's default character set.
   * <p>
   * No input is passed to the process on STDIN.
   *
   * @param cmdarray array containing the command to call and its arguments.
   * @param envp array of strings, each element of which has environment variable settings in format name=value.
   * @return The results of the execution in an ExecHelper object.
   * @throws SecurityException if a security manager exists and its checkExec method doesn't allow creation of a subprocess.
   * @throws IOException - if an I/O error occurs
   * @throws NullPointerException - if cmdarray is null
   * @throws IndexOutOfBoundsException - if cmdarray is an empty array (has length 0).
   *
   * @since ostermillerutils 1.06.00
   */
  public static ExecHelper exec(String[] cmdarray, String[] envp) throws IOException {
    return new ExecHelper(Runtime.getRuntime().exec(cmdarray, envp), null);
  }

  /**
   * Executes the specified command and arguments in a separate process, and waits for the
   * process to finish.
   * <p>
   * Output from the process is expected to be text in the system's default character set.
   * <p>
   * No input is passed to the process on STDIN.
   *
   * @param cmdarray array containing the command to call and its arguments.
   * @param envp array of strings, each element of which has environment variable settings in format name=value.
   * @param dir the working directory of the subprocess, or null if the subprocess should inherit the working directory of the current process.
   * @return The results of the execution in an ExecHelper object.
   * @throws SecurityException if a security manager exists and its checkExec method doesn't allow creation of a subprocess.
   * @throws IOException - if an I/O error occurs
   * @throws NullPointerException - if cmdarray is null
   * @throws IndexOutOfBoundsException - if cmdarray is an empty array (has length 0).
   *
   * @since ostermillerutils 1.06.00
   */
  public static ExecHelper exec(String[] cmdarray, String[] envp, File dir) throws IOException {
    return new ExecHelper(Runtime.getRuntime().exec(cmdarray, envp, dir), null);
  }

  /**
   * Executes the specified command and arguments in a separate process, and waits for the
   * process to finish.
   * <p>
   * No input is passed to the process on STDIN.
   *
   * @param cmdarray array containing the command to call and its arguments.
   * @param charset Output from the executed command is expected to be in this character set.
   * @return The results of the execution in an ExecHelper object.
   * @throws SecurityException if a security manager exists and its checkExec method doesn't allow creation of a subprocess.
   * @throws IOException - if an I/O error occurs
   * @throws NullPointerException - if cmdarray is null
   * @throws IndexOutOfBoundsException - if cmdarray is an empty array (has length 0).
   *
   * @since ostermillerutils 1.06.00
   */
  public static ExecHelper exec(String[] cmdarray, String charset) throws IOException {
    return new ExecHelper(Runtime.getRuntime().exec(cmdarray), charset);
  }

  /**
   * Executes the specified command and arguments in a separate process, and waits for the
   * process to finish.
   * <p>
   * No input is passed to the process on STDIN.
   *
   * @param cmdarray array containing the command to call and its arguments.
   * @param envp array of strings, each element of which has environment variable settings in format name=value.
   * @param charset Output from the executed command is expected to be in this character set.
   * @return The results of the execution in an ExecHelper object.
   * @throws SecurityException if a security manager exists and its checkExec method doesn't allow creation of a subprocess.
   * @throws IOException - if an I/O error occurs
   * @throws NullPointerException - if cmdarray is null
   * @throws IndexOutOfBoundsException - if cmdarray is an empty array (has length 0).
   *
   * @since ostermillerutils 1.06.00
   */
  public static ExecHelper exec(String[] cmdarray, String[] envp, String charset) throws IOException {
    return new ExecHelper(Runtime.getRuntime().exec(cmdarray, envp), charset);
  }

  /**
   * Executes the specified command and arguments in a separate process, and waits for the
   * process to finish.
   * <p>
   * No input is passed to the process on STDIN.
   *
   * @param cmdarray array containing the command to call and its arguments.
   * @param envp array of strings, each element of which has environment variable settings in format name=value.
   * @param dir the working directory of the subprocess, or null if the subprocess should inherit the working directory of the current process.
   * @param charset Output from the executed command is expected to be in this character set.
   * @return The results of the execution in an ExecHelper object.
   * @throws SecurityException if a security manager exists and its checkExec method doesn't allow creation of a subprocess.
   * @throws IOException - if an I/O error occurs
   * @throws NullPointerException - if cmdarray is null
   * @throws IndexOutOfBoundsException - if cmdarray is an empty array (has length 0).
   *
   * @since ostermillerutils 1.06.00
   */
  public static ExecHelper exec(String[] cmdarray, String[] envp, File dir, String charset) throws IOException {
    return new ExecHelper(Runtime.getRuntime().exec(cmdarray, envp, dir), charset);
  }

  /**
   * Executes the specified command using a shell.  On windows uses cmd.exe or command.exe.
   * On other platforms it uses /bin/sh.
   * <p>
   * A shell should be used to execute commands when features such as file redirection, pipes,
   * argument parsing are desired.
   * <p>
   * Output from the process is expected to be text in the system's default character set.
   * <p>
   * No input is passed to the process on STDIN.
   *
   * @param command String containing a command to be parsed by the shell and executed.
   * @return The results of the execution in an ExecHelper object.
   * @throws SecurityException if a security manager exists and its checkExec method doesn't allow creation of a subprocess.
   * @throws IOException - if an I/O error occurs
   * @throws NullPointerException - if command is null
   *
   * @since ostermillerutils 1.06.00
   */
  public static ExecHelper execUsingShell(String command) throws IOException {
    return execUsingShell(command, null);
  }

  /**
   * Executes the specified command using a shell.  On windows uses cmd.exe or command.exe.
   * On other platforms it uses /bin/sh.
   * <p>
   * A shell should be used to execute commands when features such as file redirection, pipes,
   * argument parsing are desired.
   * <p>
   * No input is passed to the process on STDIN.
   *
   * @param command String containing a command to be parsed by the shell and executed.
   * @param charset Output from the executed command is expected to be in this character set.
   * @return The results of the execution in an ExecHelper object.
   * @throws SecurityException if a security manager exists and its checkExec method doesn't allow creation of a subprocess.
   * @throws IOException - if an I/O error occurs
   * @throws NullPointerException - if command is null
   *
   * @since ostermillerutils 1.06.00
   */
  public static ExecHelper execUsingShell(String command, String charset) throws IOException {
    if (command == null) throw new NullPointerException();
    String[] cmdarray;
    String os = System.getProperty("os.name");
    if (os.equals("Windows 95") || os.equals("Windows 98") || os.equals("Windows ME")){
      cmdarray = new String[]{"command.exe", "/C", command};
    } else if (os.startsWith("Windows")){
      cmdarray = new String[]{"cmd.exe", "/C", command};
    } else {
      cmdarray = new String[]{"/bin/sh", "-c", command};
    }
    return new ExecHelper(Runtime.getRuntime().exec(cmdarray), charset);
  }

  /**
   * Take a process, record its standard error and standard out streams, wait for it to finish
   *
   * @param process process to watch
   * @throws SecurityException if a security manager exists and its checkExec method doesn't allow creation of a subprocess.
   * @throws IOException - if an I/O error occurs
   * @throws NullPointerException - if cmdarray is null
   * @throws IndexOutOfBoundsException - if cmdarray is an empty array (has length 0).
   *
   * @since ostermillerutils 1.06.00
   */
  private ExecHelper(Process process, String charset) throws IOException {
    StringBuffer output = new StringBuffer();
    StringBuffer error = new StringBuffer();

    Reader stdout;
    Reader stderr;

    if (charset == null){
      // This is one time that the system charset is appropriate,
      // don't specify a character set.
      stdout = new InputStreamReader(process.getInputStream());
      stderr = new InputStreamReader(process.getErrorStream());
    } else {
      stdout = new InputStreamReader(process.getInputStream(), charset);
      stderr = new InputStreamReader(process.getErrorStream(), charset);
    }
    char[] buffer = new char[1024];

    boolean done = false;
    boolean stdoutclosed = false;
    boolean stderrclosed = false;
    while (!done){
      boolean readSomething = false;
      // read from the process's standard output
      if (!stdoutclosed && stdout.ready()){
        readSomething = true;
        int read = stdout.read(buffer, 0, buffer.length);
        if (read < 0){
          readSomething = true;
          stdoutclosed = true;
        } else if (read > 0){
          readSomething = true;
          output.append(buffer, 0, read);
        }
      }
      // read from the process's standard error
      if (!stderrclosed && stderr.ready()){
        int read = stderr.read(buffer, 0, buffer.length);
        if (read < 0){
          readSomething = true;
          stderrclosed = true;
        } else if (read > 0){
          readSomething = true;
          error.append(buffer, 0, read);
        }
      }
      // Check the exit status only we haven't read anything,
      // if something has been read, the process is obviously not dead yet.
      if (!readSomething){
        try {
          this.status = process.exitValue();
          done = true;
        } catch (IllegalThreadStateException itx){
          // Exit status not ready yet.
          // Give the process a little breathing room.
          try {
            Thread.sleep(100);
          } catch (InterruptedException ix){
            process.destroy();
            throw new IOException("Interrupted - processes killed");
          }
        }
      }
    }

    this.output = output.toString();
    this.error = error.toString();
  }

  /**
   * The output of the job that ran.
   *
   * @since ostermillerutils 1.06.00
   */
  private String output;

  /**
   * Get the output of the job that ran.
   *
   * @return Everything the executed process wrote to its standard output as a String.
   *
   * @since ostermillerutils 1.06.00
   */
  public String getOutput(){
    return output;
  }

  /**
   * The error output of the job that ran.
   *
   * @since ostermillerutils 1.06.00
   */
  private String error;

  /**
   * Get the error output of the job that ran.
   *
   * @return Everything the executed process wrote to its standard error as a String.
   *
   * @since ostermillerutils 1.06.00
   */
  public String getError(){
    return error;
  }

  /**
   * The status of the job that ran.
   *
   * @since ostermillerutils 1.06.00
   */
  private int status;

  /**
   * Get the status of the job that ran.
   *
   * @return exit status of the executed process, by convention, the value 0 indicates normal termination.
   *
   * @since ostermillerutils 1.06.00
   */
  public int getStatus(){
    return status;
  }
}